﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace TypeBuilderFx.Core
{
    /// <summary>Base class for the emitters of types.</summary>
    public abstract class TypeEmitterBase<T_OPTIONS> where T_OPTIONS : TypeEmitterOptions
    {
        #region Inner Types
        /// <summary>Represents a method able to emit a property (either a get or a set) body.</summary>
        /// <param name="ilGenerator"></param>
        /// <param name="method"></param>
        public delegate void EmitMethodHandler(ILGeneratorExtender ilGenerator, MethodInfo method);

        /// <summary>Represents a method able to emit a method body.</summary>
        /// <param name="ilGenerator"></param>
        /// <param name="method"></param>
        /// <param name="property"/>
        public delegate void EmitPropertyHandler(ILGeneratorExtender ilGenerator, MethodInfo method, PropertyInfo property);

        /// <summary>Represents a method able to emit a method body.</summary>
        /// <param name="ilGenerator"></param>
        /// <param name="method"></param>
        /// <param name="eventInfo"/>
        public delegate void EmitEventHandler(ILGeneratorExtender ilGenerator, MethodInfo method, EventInfo eventInfo);
        #endregion

        private const BindingFlags BASE_FIND_BINDING_FLAGS =
            BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic;

        protected const string ADAPTEE_FIELD_NAME = "_adaptee";
        protected const string ITEM_PROPERTY_NAME = "Item";

        private readonly T_OPTIONS _options;

        /// <summary>Construct a type emitter from its options.</summary>
        /// <remarks>Clones the options to avoid alteration after construction.</remarks>
        /// <param name="options"></param>
        public TypeEmitterBase(T_OPTIONS options)
        {
            if (options == null)
            {
                throw new ArgumentNullException("options");
            }

            _options = (T_OPTIONS)options.Clone();
            _options.Validate();
        }

        /// <summary>Exposes the options defining the emitter.</summary>
        public T_OPTIONS Options
        {
            get { return _options; }
        }

        /// <summary>Returns an instance of a factory of the component type the type emitter is configured to produce.</summary>
        /// <param name="componentTypeName"></param>
        /// <param name="factoryTypeName"></param>
        /// <param name="moduleBuilder"></param>
        /// <returns></returns>
        public abstract object EmitFactory(
            string componentTypeName,
            string factoryTypeName,
            ModuleBuilderExtender moduleBuilder);

        #region CreateFactoryInstance Methods
        /// <summary>Helper method emitting a factory type and creating an instance of it.</summary>
        /// <remarks>Induces the constructor to use as the only public one.  If more or less than one is one, it throws.</remarks>
        /// <param name="factoryTypeName"></param>
        /// <param name="typeToCreate"></param>
        /// <param name="interfaceTypeToReturn"></param>
        /// <param name="moduleBuilder"></param>
        /// <returns></returns>
        protected object CreateFactoryInstance(
            string factoryTypeName,
            Type typeToCreate,
            Type interfaceTypeToReturn,
            ModuleBuilderExtender moduleBuilder)
        {
            ConstructorInfo[] constructorInfoList = typeToCreate.GetConstructors();

            if (constructorInfoList.Length != 1)
            {
                throw new ApplicationException(string.Format(
                    "Type {0} doesn't have one and only one constructor, it has {1}.",
                    typeToCreate.Name,
                    constructorInfoList.Length));
            }
            else
            {
                Type[] constructorParameterTypeList = Array.ConvertAll(
                    constructorInfoList[0].GetParameters(),
                    delegate(ParameterInfo info) { return info.ParameterType; });

                return CreateFactoryInstance(
                    factoryTypeName,
                    typeToCreate,
                    interfaceTypeToReturn,
                    moduleBuilder,
                    constructorParameterTypeList);
            }
        }

        /// <summary>Helper method emitting a factory type and creating an instance of it.</summary>
        /// <param name="factoryTypeName"></param>
        /// <param name="typeToCreate"></param>
        /// <param name="interfaceTypeToReturn"></param>
        /// <param name="moduleBuilder"></param>
        /// <param name="constructorParameterTypeList"></param>
        /// <returns></returns>
        protected object CreateFactoryInstance(
            string factoryTypeName,
            Type typeToCreate,
            Type interfaceTypeToReturn,
            ModuleBuilderExtender moduleBuilder,
            params Type[] constructorParameterTypeList)
        {
            ConstructorInfo constructorInfo = typeToCreate.GetConstructor(constructorParameterTypeList);

            if (constructorInfo == null)
            {
                throw new ApplicationException(string.Concat(
                    "typeToCreate doesn't have a constructor with parameters {",
                    string.Join(", ", Array.ConvertAll(constructorParameterTypeList, delegate(Type t) { return t.Name; })),
                    "}."));
            }

            Type factoryTypeDefinition = TypeHelper.GetFactoryGenericTypeDefinition(constructorParameterTypeList.Length);
            Type factoryInterfaceType =
                factoryTypeDefinition.MakeGenericType(MergeTypeList(interfaceTypeToReturn, constructorParameterTypeList));
            TypeBuilderExtender typeBuilder =
                moduleBuilder.CreateType(factoryTypeName, typeof(object), new Type[] { factoryInterfaceType });
            MethodInfo methodInfo = factoryInterfaceType.GetMethods()[0];
            ILGeneratorExtender ilGenerator = typeBuilder.DefineHiddenMethod(methodInfo);

            for (short i = 0; i != constructorParameterTypeList.Length; ++i)
            {
                ilGenerator.StackMethodParameter((short)(i + 1));
            }
            ilGenerator.StackNew(constructorInfo);
            ilGenerator.ReturnStack();

            Type factoryType = typeBuilder.TypeBuilder.CreateType();

            return Activator.CreateInstance(factoryType);
        }
        #endregion

        #region Interface Implementation
        /// <summary>
        /// Implements an interface and each of its parent interfaces (except a list of excluded interfaces)
        /// using delegates for emitting the code.
        /// </summary>
        /// <remarks>
        /// Basically this method does the plumbing and orchestration for implementing the type but leaves
        /// the method body implementation to delegates.
        /// </remarks>
        /// <param name="typeBuilder"></param>
        /// <param name="interfaceType"></param>
        /// <param name="emitGetMethod"></param>
        /// <param name="emitSetMethod"></param>
        /// <param name="emitAddEventHandlerMethod"/>
        /// <param name="emitRemoveEventHandlerMethod"/>
        /// <param name="emitMethod"></param>
        /// <param name="excludedInterfaceTypeList"></param>
        protected void ImplementInterface(
            TypeBuilderExtender typeBuilder,
            Type interfaceType,
            EmitPropertyHandler emitGetMethod,
            EmitPropertyHandler emitSetMethod,
            EmitEventHandler emitAddEventHandlerMethod,
            EmitEventHandler emitRemoveEventHandlerMethod,
            EmitMethodHandler emitMethod,
            params Type[] excludedInterfaceTypeList)
        {
            if (typeBuilder == null)
            {
                throw new ArgumentNullException("typeBuilder");
            }
            if (interfaceType == null)
            {
                throw new ArgumentNullException("interfaceType");
            }
            if (!interfaceType.IsInterface)
            {
                throw new ArgumentException(string.Format("'{0}' isn't an interface", interfaceType.Name), "interfaceType");
            }
            foreach (Type excludedInterfaceType in excludedInterfaceTypeList)
            {
                if (excludedInterfaceType == null)
                {
                    throw new ArgumentNullException("excludedInterfaceTypeList");
                }
                if (!excludedInterfaceType.IsInterface)
                {
                    throw new ArgumentException(
                        string.Format("'{0}' isn't an interface", excludedInterfaceType.Name),
                        "excludedInterfaceTypeList");
                }
            }
            Type[] allInterfaces = TypeHelper.GetAllInterfaces(interfaceType, true);

            foreach (Type subInterfaceType in allInterfaces)
            {   //  Make sure the interface isn't on the forbidden list
                if (!((IList<Type>)excludedInterfaceTypeList).Contains(subInterfaceType))
                {
                    ImplementOneInterface(
                        typeBuilder,
                        subInterfaceType,
                        emitGetMethod,
                        emitSetMethod,
                        emitAddEventHandlerMethod,
                        emitRemoveEventHandlerMethod,
                        emitMethod);
                }
            }
        }

        private void ImplementOneInterface(
            TypeBuilderExtender typeBuilder,
            Type interfaceType,
            EmitPropertyHandler emitGetMethod,
            EmitPropertyHandler emitSetMethod,
            EmitEventHandler emitAddEventHandlerMethod,
            EmitEventHandler emitRemoveEventHandlerMethod,
            EmitMethodHandler emitMethod)
        {
            SupportBaseClassTypeEmitterOptions options = Options as SupportBaseClassTypeEmitterOptions;
            Type baseTypeToUseMembers = options == null ? null : (options.IsUsingBaseClassMembers ? options.BaseType : null);
            List<MethodInfo> methodList = new List<MethodInfo>();

            methodList.AddRange(ImplementOneInterfaceEvents(
                typeBuilder,
                interfaceType,
                baseTypeToUseMembers,
                emitAddEventHandlerMethod,
                emitRemoveEventHandlerMethod));
            methodList.AddRange(ImplementOneInterfaceProperties(
                typeBuilder,
                interfaceType,
                baseTypeToUseMembers,
                emitGetMethod,
                emitSetMethod));
            ImplementOneInterfaceMethods(typeBuilder, interfaceType, baseTypeToUseMembers, emitMethod, methodList);
        }

        private void ImplementOneInterfaceMethods(
            TypeBuilderExtender typeBuilder,
            Type interfaceType,
            Type baseTypeToUseMembers,
            EmitMethodHandler emitMethod,
            IList<MethodInfo> methodList)
        {
            foreach (MethodInfo method in interfaceType.GetMethods())
            {
                MethodInfo baseMethod = GetBaseMethod(baseTypeToUseMembers, method);

                //  Ensures the method isn't a property
                if (!methodList.Contains(method))
                {
                    ILGeneratorExtender ilGenerator = typeBuilder.DefineHiddenMethod(method);

                    if (baseMethod == null)
                    {
                        if (emitMethod == null)
                        {
                            if (Options.IsEmittingThrowNotSupported)
                            {
                                ilGenerator.StackNew(typeof(NotSupportedException).GetConstructor(new Type[0]));
                                ilGenerator.Throw();
                                ilGenerator.ReturnStack();
                            }
                            else
                            {
                                throw new ArgumentNullException("emitMethod");
                            }
                        }
                        else
                        {
                            emitMethod(ilGenerator, method);
                        }
                    }
                    else
                    {
                        ilGenerator.CallMethodWithMethodParameters(baseMethod);
                    }
                }
            }
        }

        private static MethodInfo GetBaseMethod(Type type, MethodInfo method)
        {
            if (type != null)
            {
                foreach (MethodInfo baseMethod in type.GetMethods(BASE_FIND_BINDING_FLAGS))
                {   //  Find the right method instead doing a get on it, to differentiate method overloads
                    if (!baseMethod.IsPrivate
                        && baseMethod.Name == method.Name
                        && baseMethod.ReturnType == method.ReturnType
                        && TypeHelper.AreEquivalent(baseMethod.GetParameters(), method.GetParameters()))
                    {
                        return baseMethod;
                    }
                }
            }

            return null;
        }

        private MethodInfo[] ImplementOneInterfaceProperties(
            TypeBuilderExtender typeBuilder,
            Type interfaceType,
            Type baseTypeToUseMembers,
            EmitPropertyHandler emitGetMethod,
            EmitPropertyHandler emitSetMethod)
        {
            List<MethodInfo> methodList = new List<MethodInfo>();

            foreach (PropertyInfo property in interfaceType.GetProperties())
            {
                PropertyInfo baseProperty = GetBaseProperty(baseTypeToUseMembers, property);
                ILGeneratorExtender ilGeneratorGet;
                ILGeneratorExtender ilGeneratorSet;

                typeBuilder.DefineHiddenProperty(property, out ilGeneratorGet, out ilGeneratorSet);
                if (baseProperty == null)
                {
                    if (ilGeneratorGet != null)
                    {
                        if (emitGetMethod == null)
                        {
                            if (Options.IsEmittingThrowNotSupported)
                            {
                                ilGeneratorGet.StackNew(typeof(NotSupportedException).GetConstructor(new Type[0]));
                                ilGeneratorGet.Throw();
                                ilGeneratorGet.ReturnStack();
                            }
                            else
                            {
                                throw new ArgumentNullException("emitGetMethod");
                            }
                        }
                        else
                        {
                            emitGetMethod(ilGeneratorGet, property.GetGetMethod(), property);
                        }
                    }
                    if (ilGeneratorSet != null)
                    {
                        if (emitSetMethod == null)
                        {
                            if (Options.IsEmittingThrowNotSupported)
                            {
                                ilGeneratorSet.StackNew(typeof(NotSupportedException).GetConstructor(new Type[0]));
                                ilGeneratorSet.Throw();
                                ilGeneratorSet.ReturnStack();
                            }
                            else
                            {
                                throw new ArgumentNullException("emitSetMethod");
                            }
                        }
                        else
                        {
                            emitSetMethod(ilGeneratorSet, property.GetSetMethod(), property);
                        }
                    }
                }
                else
                {
                    if (ilGeneratorGet != null)
                    {
                        ilGeneratorGet.CallMethodWithMethodParameters(baseProperty.GetGetMethod(true));
                    }
                    if (ilGeneratorSet != null)
                    {
                        ilGeneratorSet.CallMethodWithMethodParameters(baseProperty.GetSetMethod(true));
                    }
                }
                if (property.CanRead)
                {
                    methodList.Add(property.GetGetMethod());
                }
                if (property.CanWrite)
                {
                    methodList.Add(property.GetSetMethod());
                }
            }

            return methodList.ToArray();
        }

        private static PropertyInfo GetBaseProperty(Type type, PropertyInfo property)
        {
            if (type != null)
            {
                PropertyInfo baseProperty = type.GetProperty(property.Name, BASE_FIND_BINDING_FLAGS);

                if (baseProperty != null
                    && baseProperty.PropertyType == property.PropertyType
                    && baseProperty.CanRead == property.CanRead
                    && baseProperty.CanWrite == property.CanWrite
                    && (!baseProperty.CanRead || !baseProperty.GetGetMethod(true).IsPrivate)
                    && (!baseProperty.CanWrite || !baseProperty.GetSetMethod(true).IsPrivate))
                {
                    return baseProperty;
                }
            }

            return null;
        }

        private MethodInfo[] ImplementOneInterfaceEvents(
            TypeBuilderExtender typeBuilder,
            Type interfaceType,
            Type baseTypeToUseMembers,
            EmitEventHandler emitAddEventHandlerMethod,
            EmitEventHandler emitRemoveEventHandlerMethod)
        {
            List<MethodInfo> methodList = new List<MethodInfo>();

            foreach (EventInfo eventInfo in interfaceType.GetEvents())
            {
                EventInfo baseEventInfo = GetBaseEvent(baseTypeToUseMembers, eventInfo);
                ILGeneratorExtender ilGeneratorAdd;
                ILGeneratorExtender ilGeneratorRemove;

                typeBuilder.DefineHiddenEvent(eventInfo, out ilGeneratorAdd, out ilGeneratorRemove);
                if (baseEventInfo == null)
                {
                    if (emitAddEventHandlerMethod == null)
                    {
                        if (Options.IsEmittingThrowNotSupported)
                        {
                            ilGeneratorAdd.StackNew(typeof(NotSupportedException).GetConstructor(new Type[0]));
                            ilGeneratorAdd.Throw();
                            ilGeneratorAdd.ReturnStack();
                        }
                        else
                        {
                            throw new ArgumentNullException("emitAddEventHandlerMethod");
                        }
                    }
                    if (emitRemoveEventHandlerMethod == null)
                    {
                        if (Options.IsEmittingThrowNotSupported)
                        {
                            ilGeneratorRemove.StackNew(typeof(NotSupportedException).GetConstructor(new Type[0]));
                            ilGeneratorRemove.Throw();
                            ilGeneratorRemove.ReturnStack();
                        }
                        else
                        {
                            throw new ArgumentNullException("emitRemoveEventHandlerMethod");
                        }
                    }

                    emitAddEventHandlerMethod(ilGeneratorAdd, eventInfo.GetAddMethod(), eventInfo);
                    emitRemoveEventHandlerMethod(ilGeneratorRemove, eventInfo.GetRemoveMethod(), eventInfo);
                }
                else
                {
                    ilGeneratorAdd.CallMethodWithMethodParameters(baseEventInfo.GetAddMethod(true));
                    ilGeneratorRemove.CallMethodWithMethodParameters(baseEventInfo.GetRemoveMethod(true));
                }
                methodList.Add(eventInfo.GetAddMethod());
                methodList.Add(eventInfo.GetRemoveMethod());
            }

            return methodList.ToArray();
        }

        private static EventInfo GetBaseEvent(Type type, EventInfo eventInfo)
        {
            if (type != null)
            {
                EventInfo baseEvent = type.GetEvent(eventInfo.Name, BASE_FIND_BINDING_FLAGS);

                if (
                    baseEvent != null
                    && baseEvent.EventHandlerType == eventInfo.EventHandlerType
                    && !baseEvent.GetAddMethod(true).IsPrivate
                    && !baseEvent.GetRemoveMethod(true).IsPrivate)
                {
                    return baseEvent;
                }
            }

            return null;
        }
        #endregion

        #region IAdapter Implementation
        /// <summary>Defines the adaptee private field.</summary>
        /// <param name="adapteeType">Type of the adaptee, the object being adapted.</param>
        /// <returns>The generic interface.</returns>
        protected Type GetAdapterInterfaceType(Type adapteeType)
        {
            Type adapterInterfaceType = typeof(IAdapter<int>).GetGenericTypeDefinition().MakeGenericType(adapteeType);

            return adapterInterfaceType;
        }

        /// <summary>Defines the adaptee private field and implement the adapter interface.</summary>
        /// <param name="typeBuilder"></param>
        /// <param name="adapteeType">Type of the adaptee, the object being adapted.</param>
        protected void ImplementAdapterInterface(TypeBuilderExtender typeBuilder, Type adapteeType)
        {
            Type adapterInterfaceType = GetAdapterInterfaceType(adapteeType);

            typeBuilder.DefinePrivateField(ADAPTEE_FIELD_NAME, adapteeType, true, false);
            ImplementInterface(typeBuilder, adapterInterfaceType, EmitAdapterGetMethod, null, null, null, null);
        }

        /// <summary>Defines a constructor taking both an adaptee and base constructor parameter type list.</summary>
        /// <param name="typeBuilder"></param>
        /// <param name="adapteeType"></param>
        /// <param name="baseType"></param>
        /// <param name="baseConstructorParameterTypeList"></param>
        protected void DefineAdapterConstructor(
            TypeBuilderExtender typeBuilder,
            Type adapteeType,
            Type baseType,
            Type[] baseConstructorParameterTypeList)
        {
            MethodParameter adapteeParameter = new MethodParameter("adaptee", adapteeType);
            ILGeneratorExtender constructorGenerator = CreateInstanceConstructor(
                typeBuilder,
                baseType,
                baseConstructorParameterTypeList,
                adapteeParameter);

            constructorGenerator.ValidateNotNullParameter(1, adapteeParameter.Name);
            //  Store adaptee in instance field
            constructorGenerator.StackMethodParameter(0);
            constructorGenerator.StackMethodParameter(1);
            constructorGenerator.StoreStackInField(ADAPTEE_FIELD_NAME);
            constructorGenerator.ReturnStack();
        }

        private void EmitAdapterGetMethod(ILGeneratorExtender ilGenerator, MethodInfo method, PropertyInfo property)
        {
            ilGenerator.StackField(ADAPTEE_FIELD_NAME);
            ilGenerator.ReturnStack();
        }
        #endregion

        #region Constructor Implementation
        /// <summary>Defines a constructor taking a base constructor parameter type list.</summary>
        /// <param name="typeBuilder"></param>
        /// <param name="baseType"></param>
        /// <param name="baseConstructorParameterTypeList"></param>
        protected void DefineInstanceConstructor(
            TypeBuilderExtender typeBuilder,
            Type baseType,
            Type[] baseConstructorParameterTypeList)
        {
            ILGeneratorExtender constructorGenerator = CreateInstanceConstructor(
                typeBuilder,
                baseType,
                baseConstructorParameterTypeList);

            constructorGenerator.ReturnStack();
        }

        /// <summary>Creates a constructor and call the base class constructor.</summary>
        /// <param name="typeBuilder"></param>
        /// <param name="baseType"></param>
        /// <param name="baseConstructorParameterTypeList"></param>
        /// <param name="beforeBaseParameters"></param>
        /// <returns></returns>
        protected ILGeneratorExtender CreateInstanceConstructor(
            TypeBuilderExtender typeBuilder,
            Type baseType,
            Type[] baseConstructorParameterTypeList,
            params MethodParameter[] beforeBaseParameters)
        {
            List<MethodParameter> parameterList = new List<MethodParameter>();

            parameterList.AddRange(beforeBaseParameters);
            for (int i = 0; i != baseConstructorParameterTypeList.Length; ++i)
            {
                parameterList.Add(new MethodParameter(
                    string.Concat("baseParam", (i + 1).ToString()),
                    baseConstructorParameterTypeList[i]));
            }

            ILGeneratorExtender constructorGenerator = typeBuilder.DefinePublicInstanceConstructor(parameterList.ToArray());
            ConstructorInfo baseConstructor = baseType.GetConstructor(baseConstructorParameterTypeList);

            //  Call Base class constructor
            constructorGenerator.StackMethodParameter(0);
            for (int i = 0; i != baseConstructorParameterTypeList.Length; ++i)
            {
                constructorGenerator.StackMethodParameter((short)(i + 2));
            }
            constructorGenerator.CallConstructor(baseConstructor);

            return constructorGenerator;
        }
        #endregion

        private static Type[] MergeTypeList(Type firstType, IEnumerable<Type> tailTypeList)
        {
            var typeList = new List<Type>();

            typeList.Add(firstType);
            typeList.AddRange(tailTypeList);

            return typeList.ToArray();
        }
    }
}